package org.shagin.jfin.sheet;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.shagin.jfin.core.Utils;
import org.shagin.jfin.data.model.Account;
import org.shagin.jfin.data.model.CostCenter;
import org.shagin.jfin.data.model.IBudget;
import org.shagin.jfin.data.model.OperationCategory;
import org.shagin.jfin.data.model.OperationCategoryLeaf;
import org.shagin.jfin.data.model.OperationRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Class Sheet.
 */
public class Sheet {

	private static final Logger log = LoggerFactory.getLogger(Sheet.class);

	public static final String COLUMN_MARKER = "*";
	public static final String COLUMN_LABEL = "Label";
	public static final String COLUMN_INCOME = "Income";
	public static final String COLUMN_EXPENSE = "Expense";
	public static final String COLUMN_SALDO = "Saldo";

	private Section secLabel;
	SheetColumnMarker colMarker;
	SheetColumnLabel colLabel;

	private Section secSaldo;
	CostCenterColumnSet totalSaldo = new CostCenterColumnSet();

	private Section secIncome;
	private Section secExpense;

	private List<Sheet> nestedSheets = new ArrayList<Sheet>();

	private List<CostCenterColumnSetEx> costCenters = new ArrayList<CostCenterColumnSetEx>();

	private SheetData data = new SheetData();

	private List<SheetRow> rowsAggr = new ArrayList<SheetRow>();
	private List<SheetRowTotalPercent> dataPercent = new ArrayList<SheetRowTotalPercent>();

	/**
	 * The Class CostCenterColumnSet.
	 */
	public static class CostCenterColumnSet {
		public SheetColumnMoney colIncome;
		public SheetColumnMoney colExpense;
		public SheetColumnMoney colSaldo;

		/**
		 * Culc saldo value.
		 *
		 * @param row the row
		 * @return the double
		 */
		private double culcSaldoValue(SheetRow row) {
			return row.getDouble(colIncome) - row.getDouble(colExpense);
		}

		/**
		 * Update saldo.
		 *
		 * @param row the row
		 */
		public void updateSaldo(SheetRow row) {
			colSaldo.setDummy(colSaldo.getDummy() + culcSaldoValue(row));

			if (!Utils.isZero(colSaldo.getDummy()))
				row.setDouble(colSaldo, new Double(colSaldo.getDummy()));
		}
	}

	/**
	 * The Class CostCenterColumnSetEx.
	 */
	public static class CostCenterColumnSetEx extends CostCenterColumnSet {
		public CostCenter costCenter;
		public Section section;
		public Map<Account, SheetColumnMoney> accounts = new HashMap<Account, SheetColumnMoney>();
	}

	/**
	 * Instantiates a new sheet.
	 */
	public Sheet() {
	}

	/**
	 * Adds the column.
	 *
	 * @param col the col
	 * @param section the section
	 */
	private void addColumn(SheetColumn<?> col, Section section) {
		section.add(col);
		col.setSection(section);
		data.addColumn(col);
	}

	/**
	 * Sets the up columns.
	 *
	 * @param dataSource the new up columns
	 */
	public void setupColumns(IBudget dataSource) {

		secLabel = new Section();
		colMarker = new SheetColumnMarker(COLUMN_MARKER);
		addColumn(colMarker, secLabel);
		colLabel = new SheetColumnLabel(COLUMN_LABEL);
		addColumn(colLabel, secLabel);

		secSaldo = new Section();
		totalSaldo.colIncome = new SheetColumnMoney(COLUMN_INCOME);
		addColumn(totalSaldo.colIncome, secSaldo);
		totalSaldo.colExpense = new SheetColumnMoney(COLUMN_EXPENSE);
		addColumn(totalSaldo.colExpense, secSaldo);
		totalSaldo.colSaldo = new SheetColumnMoney(COLUMN_SALDO);
		addColumn(totalSaldo.colSaldo, secSaldo);

		secIncome = new Section();
		appendRules(dataSource.getOperationRuleDomain().getIncomes(), secIncome);

		secExpense = new Section();
		appendRules(dataSource.getOperationRuleDomain().getExpenses(),
				secExpense);

		log.debug("append CostCenter columns...");
		for (CostCenter costCenter : dataSource.getCostCenterDomain().getAll()) {
			CostCenterColumnSetEx ccc = new CostCenterColumnSetEx();
			ccc.costCenter = costCenter;
			ccc.section = new Section();
			costCenters.add(ccc);
			for (Account account : costCenter.getAccounts()) {
				SheetColumnMoney col = new SheetColumnMoney(account.getId());
				ccc.accounts.put(account, col);
				addColumn(col, ccc.section);
			}
			ccc.colExpense = new SheetColumnMoney(costCenter.getId()
					+ COLUMN_EXPENSE);
			ccc.colIncome = new SheetColumnMoney(costCenter.getId()
					+ COLUMN_INCOME);
			ccc.colSaldo = new SheetColumnMoney(costCenter.getId()
					+ COLUMN_SALDO);

			addColumn(ccc.colExpense, ccc.section);
			addColumn(ccc.colIncome, ccc.section);
			addColumn(ccc.colSaldo, ccc.section);
		}
	}

	/**
	 * Adds the operation category.
	 *
	 * @param cols the cols
	 * @param category the category
	 */
	private void addOperationCategory(List<SheetColumn<?>> cols,
			OperationCategory category) {
		cols.add(new SheetColumnCategory(category));

		if (category instanceof OperationCategoryLeaf) {
			OperationCategoryLeaf categoryLeaf = (OperationCategoryLeaf) category;
			for (OperationRule rule : categoryLeaf.getRules())
				findOrCreateColumn(rule, cols);
		}
		for (OperationCategory child : category.getChilds()) {
			addOperationCategory(cols, child);
		}
	}

	/**
	 * Append rules.
	 *
	 * @param rules the rules
	 * @param section the section
	 */
	private void appendRules(List<OperationRule> rules, Section section) {
		List<SheetColumn<?>> cols = new ArrayList<SheetColumn<?>>();
		for (OperationCategory root : OperationRule.getRoots(rules))
			addOperationCategory(cols, root);

		Collections.reverse(cols);
		for (SheetColumn<?> col : cols)
			addColumn(col, section);
	}

	/**
	 * Find or create column.
	 *
	 * @param rule the rule
	 * @param cols the cols
	 * @return the sheet column operation rule
	 */
	private SheetColumnOperationRule findOrCreateColumn(OperationRule rule,
			List<SheetColumn<?>> cols) {
		SheetColumnOperationRule result = findCol(rule.getId(), cols);

		if (null == result) {
			result = new SheetColumnOperationRule(rule.getId());
			cols.add(result);
		}
		result.addRule(rule);
		return result;
	}

	/**
	 * Find col.
	 *
	 * @param id the id
	 * @param cols the cols
	 * @return the sheet column operation rule
	 */
	private static SheetColumnOperationRule findCol(String id,
			List<SheetColumn<?>> cols) {
		SheetColumnOperationRule result = null;

		for (SheetColumn<?> col : cols) {
			if ((col instanceof SheetColumnOperationRule)
					&& id.equalsIgnoreCase(col.getId())) {
				result = (SheetColumnOperationRule) col;
				break;
			}
		}

		return result;
	}

	/**
	 * Fund column by rule.
	 *
	 * @param rule the rule
	 * @return the sheet column operation rule
	 */
	public SheetColumnOperationRule fundColumnByRule(OperationRule rule) {
		SheetColumnOperationRule column = null;
		for (SheetColumn<?> col : data.getColumns()) {
			if (col instanceof SheetColumnOperationRule) {
				SheetColumnOperationRule c = (SheetColumnOperationRule) col;
				if (c.containRule(rule)) {
					column = c;
					break;
				}
			}
		}
		return column;
	}

	/**
	 * Gets the sheet column categories.
	 *
	 * @return the sheet column categories
	 */
	public List<SheetColumnCategory> getSheetColumnCategories() {
		List<SheetColumnCategory> result = new ArrayList<SheetColumnCategory>();
		for (SheetColumn<?> col : data.getColumns()) {
			if (col instanceof SheetColumnCategory) {
				result.add((SheetColumnCategory) col);
			}
		}
		return result;
	}

	/**
	 * Gets the rows.
	 *
	 * @return the rows
	 */
	public List<SheetRow> getRows() {
		return data.getRows();
	}

	/**
	 * Gets the aggr rows.
	 *
	 * @return the aggr rows
	 */
	public List<SheetRow> getAggrRows() {
		return rowsAggr;
	}

	/**
	 * Creates the row.
	 *
	 * @return the sheet row data
	 */
	public SheetRowData createRow() {
		return new SheetRowData();
	}

	/**
	 * Creates the row sub total.
	 *
	 * @return the sheet row sub total
	 */
	public SheetRowSubTotal createRowSubTotal() {
		return new SheetRowSubTotal();
	}

	/**
	 * Creates the row total.
	 *
	 * @return the sheet row total
	 */
	public SheetRowTotal createRowTotal() {
		return new SheetRowTotal(rowsAggr, colLabel);
	}

	/**
	 * Adds the row.
	 *
	 * @param row the row
	 */
	public void addRow(SheetRow row) {
		data.addRow(row);
	}

	/**
	 * Adds the sub total row.
	 *
	 * @param row the row
	 */
	public void addSubTotalRow(SheetRowSubTotal row) {
		addRow(row);
		rowsAggr.add(row);
	}

	/**
	 * Adds the total percent row.
	 *
	 * @param row the row
	 */
	public void addTotalPercentRow(SheetRowTotalPercent row) {
		dataPercent.add(row);
	}

	/**
	 * Gets the column index.
	 *
	 * @param id the id
	 * @return the column index
	 */
	public int getColumnIndex(String id) {
		int result = -1;
		for (int i = 0; i < data.getColumnCount(); i++) {
			if (id.equalsIgnoreCase(data.getColumn(i).getId())) {
				result = i;
				break;
			}
		}
		return result;
	}

	/**
	 * Gets the income section.
	 *
	 * @return the income section
	 */
	public Section getIncomeSection() {
		return secIncome;
	}

	/**
	 * Gets the expense section.
	 *
	 * @return the expense section
	 */
	public Section getExpenseSection() {
		return secExpense;
	}

	/**
	 * Gets the data percent.
	 *
	 * @return the data percent
	 */
	public List<SheetRowTotalPercent> getDataPercent() {
		return dataPercent;
	}

	/**
	 * Gets the cost centers.
	 *
	 * @return the cost centers
	 */
	public List<CostCenterColumnSetEx> getCostCenters() {
		return costCenters;
	}

	/**
	 * Gets the total saldo.
	 *
	 * @return the total saldo
	 */
	public CostCenterColumnSet getTotalSaldo() {
		return totalSaldo;
	}

	/**
	 * Gets the data.
	 *
	 * @return the data
	 */
	public SheetData getData() {
		return data;
	}

	/**
	 * Gets the nested sheets.
	 *
	 * @return the nested sheets
	 */
	public List<Sheet> getNestedSheets() {
		return Collections.unmodifiableList(nestedSheets);
	}

	/**
	 * Adds the nested sheets.
	 *
	 * @param sheet the sheet
	 */
	public void addNestedSheets(Sheet sheet) {
		nestedSheets.add(sheet);
	}
}
